Hooray! It's opposite day. Linked lists go the opposite way today.
Write a function for reversing a linked list.↴
Quick reference
Worst Case
space
O(n)
prepend
O(1)
append
O(1)
lookup
O(n)
insert
O(n)
delete
O(n)
A linked list organizes items sequentially, with each item
storing a pointer to the next one.
Picture a linked list like a chain of paperclips linked together. It's quick to add another paperclip to the top or bottom. It's even quick to insert one in the middle—just disconnect the chain at the middle link, add the new paperclip, then reconnect the other half.
An item in a linked list is called a node. The first node is called the head. The last node is called the tail.
Confusingly, sometimes people use the word tail to refer to "the whole rest of the list after the head."
Unlike an array, consecutive items in a linked list are not
necessarily next to each other in memory.
Strengths:
Fast operations on the ends. Adding elements
at either end of a linked list is O(1).
Removing the first element is also O(1).
Flexible size. There's no need to specify
how many elements you're going to store ahead of time. You
can keep adding elements as long as there's enough space
on the machine.
Weaknesses:
Costly lookups. To access or edit an item
in a linked list, you have
to take O(i) time to walk
from the head of the list to the ith
item.
Uses:
Stacks and queues only need fast operations on the ends, so linked lists are ideal.
In Python 3.6
Most languages (including Python 3.6)
don't provide a linked list implementation. Assuming we've already
implemented our own, here's how we'd construct the linked list
above:
a = LinkedListNode(5)
b = LinkedListNode(1)
c = LinkedListNode(9)
a.next = b
b.next = c
Doubly Linked Lists
In a basic linked list, each item stores a single pointer to the
next element.
In a doubly linked list, items have pointers to
the next and the previous nodes.
Doubly linked lists allow us to traverse our
list backwards. In a singly linked list, if you
just had a pointer to a node in the middle of a list,
there would be no way to know what nodes came before it.
Not a problem in a doubly linked list.
Array items are always located right next to each other in computer memory, but linked list nodes can be scattered all over.
So iterating through a linked list is usually quite a bit slower than iterating through the items in an array, even though they're both theoretically O(n) time.
Do it in place.↴
An in-placefunction
modifies data structures or objects outside of its
own stack frame↴
Overview
The call stack is what a program uses to keep
track of function calls. The call stack is
made up of stack frames—one for
each function call.
For instance, say we called a function that
rolled two dice and printed the sum.
defroll_die():return random.randint(1,6)defroll_two_and_sum():
total =0
total += roll_die()
total += roll_die()print(total)
roll_two_and_sum()
First, our program calls roll_two_and_sum(). It goes
on the call stack:
roll_two_and_sum()
That function calls roll_die(), which gets pushed
on to the top of the call stack:
roll_die()
roll_two_and_sum()
Inside of roll_die(), we call random.randint().
Here's what our call stack looks like then:
random.randint()
roll_die()
roll_two_and_sum()
When random.randint() finishes, we return back to
roll_die() by removing
("popping") random.randint()'s stack frame.
roll_die()
roll_two_and_sum()
Same thing when roll_die() returns:
roll_two_and_sum()
We're not done yet! roll_two_and_sum()
calls roll_die()again:
roll_die()
roll_two_and_sum()
Which calls random.randint() again:
random.randint()
roll_die()
roll_two_and_sum()
random.randint() returns, then roll_die() returns,
putting us back in roll_two_and_sum():
roll_two_and_sum()
Which calls print()():
print()()
roll_two_and_sum()
What's stored in a stack frame?
What actually goes in a function's
stack frame?
A stack frame usually stores:
Local variables
Arguments passed into the function
Information about the caller's stack frame
The return address—what the program should do
after the function returns (i.e.: where it should "return
to"). This is usually somewhere in the middle of the caller's
code.
Some of the specifics vary between processor architectures. For
instance, AMD64 (64-bit x86) processors pass some arguments in
registers and some on the call stack. And, ARM processors (common
in phones) store the return address in a special register instead
of putting it on the call stack.
The Space Cost of Stack Frames
Each function call creates its own stack
frame, taking up space on the call stack. That's important
because it can impact the space complexity of an algorithm.
Especially when we use recursion.
For example, if we wanted to multiply all the numbers
between 1 and n,
we could use this recursive approach:
defproduct_1_to_n(n):return1if n <=1else n * product_1_to_n(n -1)
What would the call stack look like
when n = 10?
First, product_1_to_n() gets called
with n = 10:
product_1_to_n() n = 10
This calls product_1_to_n() with
n = 9.
product_1_to_n() n = 9
product_1_to_n() n = 10
Which calls product_1_to_n()
with n = 8.
product_1_to_n() n = 8
product_1_to_n() n = 9
product_1_to_n() n = 10
And so on until we get to n = 1.
product_1_to_n() n = 1
product_1_to_n() n = 2
product_1_to_n() n = 3
product_1_to_n() n = 4
product_1_to_n() n = 5
product_1_to_n() n = 6
product_1_to_n() n = 7
product_1_to_n() n = 8
product_1_to_n() n = 9
product_1_to_n() n = 10
Look at the size of all those stack frames! The entire call stack
takes up O(n) space. That's right—we
have an O(n) space cost even though
our function itself doesn't create any data
structures!
What if we'd used an iterative approach instead of a recursive one?
defproduct_1_to_n(n):# We assume n >= 1
result =1for num in range(1, n +1):
result *= num
return result
This version takes a constant amount of space. At the beginning of the loop,
the call stack looks like this:
product_1_to_n() n = 10, result = 1, num = 1
As we iterate through the loop, the local variables change, but we
stay in the same stack frame because we don't call any other
functions.
product_1_to_n() n = 10, result = 2, num = 2
product_1_to_n() n = 10, result = 6, num = 3
product_1_to_n() n = 10, result = 24, num = 4
In general, even though the compiler or interpreter will take
care of managing the call stack for you, it's important to consider the
depth of the call stack when analyzing the space complexity of an
algorithm.
Be especially careful with recursive functions!
They can end up building huge call stacks.
What happens if we run out of space? It's a stack
overflow! In Python 3.6, you'll get
a RecursionError.
If the very last thing
a function does is call
another function, then its stack frame
might not be needed any more. The functioncould free up its stack frame before doing its final
call, saving space.
This is called tail call optimization
(TCO). If a recursive function is optimized with TCO, then it
may not end up with a big call stack.
In general, most languages don't provide TCO. Scheme
is one of the few languages that guarantee tail call
optimization. Some Ruby, C, and Javascript
implementations may do it. Python and Java decidedly
don't.
(i.e.: stored on
the process heap or in
the stack frame of a calling function). Because of this, the
changes made by the function remain after
the call completes.
In-place algorithms are sometimes called
destructive, since the original input is
"destroyed" (or modified) during
the function call.
Careful: "In-place" does not mean "without
creating any additional variables!" Rather, it means
"without creating a new copy of the input." In general, an
in-place function will only create
additional variables that are O(1) space.
An out-of-placefunction
doesn't make any changes that are visible to
other functions. Usually,
those functions copy any data structures or objects
before manipulating and changing them.
In many languages, primitive values (integers,
floating point numbers, or characters) are copied when passed as
arguments, and more complex data structures
(lists, heaps, or hash tables) are
passed by
reference. This is what Python does.
Here are two functions that do the same
operation on a list, except one is
in-place and the other is out-of-place:
defsquare_list_in_place(int_list):for index, element in enumerate(int_list):
int_list[index]*= element
# NOTE: no need to return anything - we modified# int_list in placedefsquare_list_out_of_place(int_list):# We allocate a new list with the length of the input list
squared_list =[None]* len(int_list)for index, element in enumerate(int_list):
squared_list[index]= element **2return squared_list
Working in-place is a good way to save time and
space. An in-place algorithm avoids the cost of
initializing or copying data structures, and it usually has
an O(1) space cost.
But be careful: an in-place algorithm can cause side effects.
Your input is "destroyed" or "altered," which can affect
code outside of your function. For
example:
Generally, out-of-place algorithms are considered safer
because they avoid side effects. You should only use an
in-place algorithm if you're space constrained or
you're positive you don't need the original input
anymore, even for debugging.
Your function will have one input: the head of the list.
Your function should return the new head of the list.
Here's a sample linked list node class:
classLinkedListNode(object):def__init__(self, value):
self.value = value
self.next = None
Gotchas
We can do this in O(1) space. So don't make a new list; use the existing list nodes!
We can do this is in O(n) time.
Careful—even the right approach will fail if done in the wrong order.
Try drawing a picture of a small linked list and running your function by hand. Does it actually work?
The most obvious edge cases are:
the list has 0 elements
the list has 1 element
Does your function correctly handle those cases?
Breakdown
Our first thought might be to build our reversed list "from the beginning," starting with the head of the final reversed linked list.
The head of the reversed list will be the tail of the input list. To get to that node we'll have to walk through the whole list once (O(n) time). And that's just to get started.
That seems inefficient. Can we reverse the list while making just one walk from head to tail of the input list?
We can reverse the list by changing the next pointer of each node. Where should each node's next pointer...point?
Each node's next pointer should point to the previous node.
How can we move each node's next pointer to its previous node in one pass from head to tail of our current list?
Solution
In one pass from head to tail of our input list, we point each node's next pointer to the previous item.
The order of operations is important here! We're careful to copy current_node.next into nextbefore setting current_node.next to previous_node. Otherwise "stepping forward" at the end could actually mean stepping back to previous_node!
defreverse(head_of_list):
current_node = head_of_list
previous_node = None
next_node = None
# Until we have 'fallen off' the end of the listwhile current_node:# Copy a pointer to the next element# before we overwrite current_node.next
next_node = current_node.next
# Reverse the 'next' pointer
current_node.next = previous_node
# Step forward in the list
previous_node = current_node
current_node = next_node
return previous_node
We return previous_node because when we exit the list, current_node is None. Which means that the last node we visited—previous_node—was the tail of the original list, and thus the head of our reversed list.
Complexity
O(n) time and O(1) space. We pass over the list only once, and maintain a constant number of variables in memory.
Bonus
This in-place↴
An in-placefunction
modifies data structures or objects outside of its
own stack frame↴
Overview
The call stack is what a program uses to keep
track of function calls. The call stack is
made up of stack frames—one for
each function call.
For instance, say we called a function that
rolled two dice and printed the sum.
defroll_die():return random.randint(1,6)defroll_two_and_sum():
total =0
total += roll_die()
total += roll_die()print(total)
roll_two_and_sum()
First, our program calls roll_two_and_sum(). It goes
on the call stack:
roll_two_and_sum()
That function calls roll_die(), which gets pushed
on to the top of the call stack:
roll_die()
roll_two_and_sum()
Inside of roll_die(), we call random.randint().
Here's what our call stack looks like then:
random.randint()
roll_die()
roll_two_and_sum()
When random.randint() finishes, we return back to
roll_die() by removing
("popping") random.randint()'s stack frame.
roll_die()
roll_two_and_sum()
Same thing when roll_die() returns:
roll_two_and_sum()
We're not done yet! roll_two_and_sum()
calls roll_die()again:
roll_die()
roll_two_and_sum()
Which calls random.randint() again:
random.randint()
roll_die()
roll_two_and_sum()
random.randint() returns, then roll_die() returns,
putting us back in roll_two_and_sum():
roll_two_and_sum()
Which calls print()():
print()()
roll_two_and_sum()
What's stored in a stack frame?
What actually goes in a function's
stack frame?
A stack frame usually stores:
Local variables
Arguments passed into the function
Information about the caller's stack frame
The return address—what the program should do
after the function returns (i.e.: where it should "return
to"). This is usually somewhere in the middle of the caller's
code.
Some of the specifics vary between processor architectures. For
instance, AMD64 (64-bit x86) processors pass some arguments in
registers and some on the call stack. And, ARM processors (common
in phones) store the return address in a special register instead
of putting it on the call stack.
The Space Cost of Stack Frames
Each function call creates its own stack
frame, taking up space on the call stack. That's important
because it can impact the space complexity of an algorithm.
Especially when we use recursion.
For example, if we wanted to multiply all the numbers
between 1 and n,
we could use this recursive approach:
defproduct_1_to_n(n):return1if n <=1else n * product_1_to_n(n -1)
What would the call stack look like
when n = 10?
First, product_1_to_n() gets called
with n = 10:
product_1_to_n() n = 10
This calls product_1_to_n() with
n = 9.
product_1_to_n() n = 9
product_1_to_n() n = 10
Which calls product_1_to_n()
with n = 8.
product_1_to_n() n = 8
product_1_to_n() n = 9
product_1_to_n() n = 10
And so on until we get to n = 1.
product_1_to_n() n = 1
product_1_to_n() n = 2
product_1_to_n() n = 3
product_1_to_n() n = 4
product_1_to_n() n = 5
product_1_to_n() n = 6
product_1_to_n() n = 7
product_1_to_n() n = 8
product_1_to_n() n = 9
product_1_to_n() n = 10
Look at the size of all those stack frames! The entire call stack
takes up O(n) space. That's right—we
have an O(n) space cost even though
our function itself doesn't create any data
structures!
What if we'd used an iterative approach instead of a recursive one?
defproduct_1_to_n(n):# We assume n >= 1
result =1for num in range(1, n +1):
result *= num
return result
This version takes a constant amount of space. At the beginning of the loop,
the call stack looks like this:
product_1_to_n() n = 10, result = 1, num = 1
As we iterate through the loop, the local variables change, but we
stay in the same stack frame because we don't call any other
functions.
product_1_to_n() n = 10, result = 2, num = 2
product_1_to_n() n = 10, result = 6, num = 3
product_1_to_n() n = 10, result = 24, num = 4
In general, even though the compiler or interpreter will take
care of managing the call stack for you, it's important to consider the
depth of the call stack when analyzing the space complexity of an
algorithm.
Be especially careful with recursive functions!
They can end up building huge call stacks.
What happens if we run out of space? It's a stack
overflow! In Python 3.6, you'll get
a RecursionError.
If the very last thing
a function does is call
another function, then its stack frame
might not be needed any more. The functioncould free up its stack frame before doing its final
call, saving space.
This is called tail call optimization
(TCO). If a recursive function is optimized with TCO, then it
may not end up with a big call stack.
In general, most languages don't provide TCO. Scheme
is one of the few languages that guarantee tail call
optimization. Some Ruby, C, and Javascript
implementations may do it. Python and Java decidedly
don't.
(i.e.: stored on
the process heap or in
the stack frame of a calling function). Because of this, the
changes made by the function remain after
the call completes.
In-place algorithms are sometimes called
destructive, since the original input is
"destroyed" (or modified) during
the function call.
Careful: "In-place" does not mean "without
creating any additional variables!" Rather, it means
"without creating a new copy of the input." In general, an
in-place function will only create
additional variables that are O(1) space.
An out-of-placefunction
doesn't make any changes that are visible to
other functions. Usually,
those functions copy any data structures or objects
before manipulating and changing them.
In many languages, primitive values (integers,
floating point numbers, or characters) are copied when passed as
arguments, and more complex data structures
(lists, heaps, or hash tables) are
passed by
reference. This is what Python does.
Here are two functions that do the same
operation on a list, except one is
in-place and the other is out-of-place:
defsquare_list_in_place(int_list):for index, element in enumerate(int_list):
int_list[index]*= element
# NOTE: no need to return anything - we modified# int_list in placedefsquare_list_out_of_place(int_list):# We allocate a new list with the length of the input list
squared_list =[None]* len(int_list)for index, element in enumerate(int_list):
squared_list[index]= element **2return squared_list
Working in-place is a good way to save time and
space. An in-place algorithm avoids the cost of
initializing or copying data structures, and it usually has
an O(1) space cost.
But be careful: an in-place algorithm can cause side effects.
Your input is "destroyed" or "altered," which can affect
code outside of your function. For
example:
Generally, out-of-place algorithms are considered safer
because they avoid side effects. You should only use an
in-place algorithm if you're space constrained or
you're positive you don't need the original input
anymore, even for debugging.
reversal destroys the input linked list. What if we wanted to keep a copy of the original linked list? Write a function for reversing a linked list out-of-place.
What We Learned
It's one of those problems where, even once you know the procedure, it's hard to write a bug-free solution. Drawing it out helps a lot. Write out a sample linked list and walk through your code by hand, step by step, running each operation on your sample input to see if the final output is what you expect. This is a great strategy for any coding interview question.
Actually, we don't support password-based login. Never have. Just the OAuth methods above. Why?
It's easy and quick. No "reset password" flow. No password to forget.
It lets us avoid storing passwords that hackers could access and use to try to log into our users' email or bank accounts.
It makes it harder for one person to share a paid Interview Cake account with multiple people.
“Once I started using Interview Cake, it became my primary resource for algorithm problems. The breakdown explanations are just excellent—so thorough and clear.
—
An